1 /* 2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021 3 License: [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License]. 4 Authors: Marcelo S. N. Mancini 5 6 Copyright Marcelo S. N. Mancini 2018 - 2021. 7 Distributed under the CC BY-4.0 License. 8 (See accompanying file LICENSE.txt or copy at 9 https://creativecommons.org/licenses/by/4.0/ 10 */ 11 module hip.filesystem.hipfs; 12 13 public import hip.api.filesystem.hipfs; 14 import hip.util.reflection; 15 16 /** 17 * Returns whether if the path attempts to exit the initial one. 18 * Params: 19 * initial = 20 * toAppend = 21 * Returns: 22 */ 23 private pure bool validatePath(string initial, string toAppend) 24 { 25 import hip.util.array:lastIndexOf; 26 import hip.util.string:splitRange, PathString; 27 import hip.util.system : sanitizePath; 28 29 if(initial.length != 0 && initial[$-1] == '/') 30 initial = initial[0..$-1]; 31 scope string appends = toAppend.sanitizePath; 32 33 PathString newPath = PathString(initial.sanitizePath); 34 35 foreach(a; splitRange(appends, "/")) 36 { 37 if(a == "" || a == ".") 38 continue; 39 if(a == "..") 40 { 41 long lastInd = newPath.toString.lastIndexOf('/'); 42 if(lastInd == -1) 43 continue; 44 newPath = newPath[0..cast(uint)lastInd]; 45 } 46 else 47 { 48 newPath~= "/"; 49 newPath~= a; 50 } 51 } 52 for(int i = 0; i < initial.length; i++) 53 if(initial[i] != newPath[i]) 54 return false; 55 return true; 56 } 57 58 ///Function is implemented AppDelegate.m 59 version(AppleOS) 60 private extern(C) const(char*) hipGetResourcesPath(); 61 62 abstract class HipFile : IHipFileItf 63 { 64 immutable FileMode mode; 65 immutable string path; 66 ulong size; 67 ulong cursor; 68 @disable this(); 69 this(string path, FileMode mode) 70 { 71 this.mode = mode; 72 this.path = path; 73 open(path, mode); 74 this.size = getSize(); 75 } 76 ///Whence is the same from libc 77 long seek(long count, int whence = SEEK_CUR) 78 { 79 switch(whence) 80 { 81 default: 82 case SEEK_CUR: 83 cursor+= count; 84 break; 85 case SEEK_END: 86 cursor = size + count; 87 break; 88 case SEEK_SET: 89 cursor = count; 90 break; 91 } 92 return cast(long)cursor; 93 } 94 95 T[] rawRead(T)(T[] buffer) 96 { 97 read(cast(void*)buffer.ptr,buffer.length); 98 return buffer; 99 } 100 } 101 102 103 class HipFSPromise : IHipFSPromise 104 { 105 string filename; 106 FileReadResult delegate(in ubyte[] data)[] onSuccessList; 107 void delegate(string err)[] onErrorList; 108 ubyte[] data; 109 bool finished = false; 110 FileReadResult result = FileReadResult.keep; 111 this(string filename){this.filename = filename;} 112 IHipFSPromise addOnSuccess(FileReadResult delegate(in ubyte[] data) onSuccess) 113 { 114 if(result != FileReadResult.keep) 115 throw new Exception("HipFSPromise Error: "~filename~" data was already freed, but addOnSuccess is being called."); 116 if(finished) 117 { 118 if(onSuccess(data) == FileReadResult.free) 119 { 120 result = FileReadResult.free; 121 dispose(); 122 } 123 } 124 else 125 onSuccessList~=onSuccess; 126 return this; 127 } 128 IHipFSPromise addOnError(void delegate(string error) onError) 129 { 130 if(finished) 131 { 132 if(data.length == 0 && result != FileReadResult.free) 133 onError("No data"); 134 } 135 else 136 onErrorList~= onError; 137 return this; 138 } 139 FileReadResult setFinished(ubyte[] data) 140 { 141 if(finished) 142 assert(false, "HipFSPromise was already resolved."); 143 this.data = data; 144 this.finished = true; 145 FileReadResult r = FileReadResult.keep; 146 if(data) foreach(success; onSuccessList) 147 r|= success(data); 148 else foreach(err; onErrorList) 149 err("Could not read file"); 150 151 if(r == FileReadResult.free) 152 dispose(); 153 return result = r; 154 } 155 bool resolved() const{return finished;} 156 157 void dispose() 158 { 159 version(WebAssembly) {} 160 else 161 { 162 import core.memory; 163 GC.free(data.ptr); 164 data = null; 165 } 166 } 167 } 168 169 /** 170 * FileSystem access for specific platforms. 171 */ 172 class HipFileSystemImplementation : IHipFS 173 { 174 protected string defPath; 175 protected string initialPath = ""; 176 protected string combinedPath; 177 protected bool isInstalled; 178 protected IHipFileSystemInteraction fs; 179 protected size_t filesReadingCount = 0; 180 181 protected bool function(string path, out string errMessage)[] extraValidations; 182 183 version(Android){import hip.filesystem.systems.android;} 184 else version(UWP){import hip.filesystem.systems.uwp;} 185 else version(WebAssembly){import hip.filesystem.systems.browser;} 186 else version(PSVita){import hip.filesystem.systems.cstd;} 187 else version(CustomRuntimeTest){import hip.filesystem.systems.cstd;} 188 else version(HipDStdFile){import hip.filesystem.systems.dstd;} 189 else {import hip.filesystem.systems.cstd;} 190 191 public void initializeAbsolute() 192 { 193 if(fs is null) 194 { 195 version(Android){fs = new HipAndroidFileSystemInteraction();} 196 else version(UWP){fs = new HipUWPileSystemInteraction();} 197 else version(PSVita){fs = new HipCStdioFileSystemInteraction();} 198 else version(CustomRuntimeTest){fs = new HipCStdioFileSystemInteraction();} 199 else version(WebAssembly){fs = new HipBrowserFileSystemInteraction();} 200 else 201 { 202 version(HipDStdFile){}else{static assert(false, "HipDStdFile should be marked to be used.");} 203 fs = new HipStdFileSystemInteraction(); 204 } 205 } 206 } 207 208 209 public void install(string path) 210 { 211 import hip.util.system : sanitizePath; 212 if(!isInstalled) 213 { 214 initialPath = path.sanitizePath; 215 setPath(""); 216 isInstalled = true; 217 } 218 } 219 /** 220 * This function may be refactored in future since having different 221 * directories to resources to writeable paths is becoming more common 222 */ 223 version(AppleOS) 224 public string getResourcesPath() 225 { 226 import core.stdc.string; 227 auto str = hipGetResourcesPath; 228 return cast(string)str[0..strlen(str)]; 229 } 230 231 232 public void install(string path, 233 bool function(string path, out string errMessage)[] validations ...) 234 { 235 import hip.util.system : sanitizePath; 236 if(!isInstalled) 237 { 238 install(path); 239 foreach (v; validations){extraValidations~=v;} 240 } 241 } 242 public string getPath(string path) 243 { 244 import hip.util.path:joinPath; 245 import hip.util.system : sanitizePath; 246 import hip.console.log; 247 248 if(combinedPath) 249 return joinPath(combinedPath, path.sanitizePath); 250 return path.sanitizePath; 251 } 252 public bool isPathValidExtra(string path) 253 { 254 import hip.error.handler; 255 import hip.util.system : sanitizePath; 256 path = path.sanitizePath; 257 string err; 258 foreach (bool function(string, out string) validation; extraValidations) 259 { 260 if(!validation(path, err)) 261 { 262 ErrorHandler.showErrorMessage("HipFileSystem validation error", 263 "Path '"~path~"' failed at validation with error: '"~err~"'."); 264 return false; 265 } 266 } 267 return true; 268 } 269 270 public bool isPathValid(string path, bool expectsFile = true, bool shouldVerify = true) 271 { 272 import hip.error.handler; 273 import hip.util.string; 274 if(!isInstalled) return false; 275 PathString s = PathString(defPath, path); 276 if(!validatePath(initialPath, s.toString)) 277 { 278 ErrorHandler.showErrorMessage("Path failed default validation: can't reference external path.", path); 279 return false; 280 } 281 if(shouldVerify) 282 { 283 if((expectsFile && !HipFS.absoluteIsFile(path)) || (!expectsFile && !HipFS.absoluteIsDir(path))) 284 { 285 ErrorHandler.showErrorMessage("Path failed default validation: Expected '"~ (expectsFile ? "file" : "directory") ~ 286 "' but received "~ (expectsFile ? "'directory'" : "'file'"), path); 287 return false; 288 } 289 } 290 291 return isPathValidExtra(path); 292 } 293 294 public bool setPath(string path) 295 { 296 import hip.util.path:joinPath; 297 import hip.util.system : sanitizePath; 298 import hip.console.log; 299 if(path) 300 { 301 defPath = path.sanitizePath; 302 combinedPath = joinPath(initialPath, defPath); 303 } 304 else 305 combinedPath = initialPath; 306 return validatePath(initialPath, combinedPath); 307 } 308 309 private void defaultErrorHandler(string err = "") 310 { 311 import hip.error.handler; 312 filesReadingCount--; 313 ErrorHandler.assertExit(false, "HipFS Error: "~err); 314 } 315 316 ///TODO: Fix API. It currently does not work with sync and async at the same way. 317 /// It needs to specify both onSuccess and onError before being able to establish if it is possible to keep or not the memory. 318 public IHipFSPromise read(string path) 319 { 320 import hip.console.log; 321 hiplog("Required path ", getPath(path)); 322 path = getPath(path); 323 if(!isPathValid(path)) 324 { 325 hiplog("Invalid path ",path," received."); 326 return null; 327 } 328 filesReadingCount++; 329 330 HipFSPromise promise = new HipFSPromise(path); 331 332 fs.read(path, (ubyte[] data) 333 { 334 filesReadingCount--; 335 return promise.setFinished(data); 336 }, (string err) 337 { 338 promise.setFinished(null); 339 defaultErrorHandler(err); 340 }); 341 342 return promise; 343 } 344 345 public IHipFSPromise readText(string path) 346 { 347 IHipFSPromise ret = read(path); 348 // if(ret) 349 // { 350 // import std.utf; 351 // output = toUTF8((cast(string)data)); 352 // } 353 return ret; 354 } 355 356 public bool write(string path, const(void)[] data) 357 { 358 if(!isPathValid(path)) 359 return false; 360 return fs.write(getPath(path), data); 361 } 362 363 364 public bool exists(string path){return isPathValid(path) && fs.exists(getPath(path));} 365 public bool remove(string path) 366 { 367 if(!isPathValid(path)) 368 return false; 369 return fs.remove(getPath(path)); 370 } 371 372 public string getcwd() 373 { 374 return getPath(""); 375 } 376 377 public bool absoluteExists(string path){return fs.exists(path);} 378 public bool absoluteIsDir(string path){return fs.isDir(path);} 379 public bool absoluteIsFile(string path){return fs.isFile(path);} 380 public bool absoluteRemove(string path){return fs.remove(path);} 381 public bool absoluteWrite(string path, const(void)[] data){return fs.write(path, data);} 382 public bool absoluteRead(string path, out void[] output) 383 { 384 ///This may need to be refactored in the future. 385 // import std.functional:toDelegate; 386 return fs.read(path, (void[] data){output = data; return FileReadResult.keep;}, (err) => defaultErrorHandler(err)); 387 } 388 @ExportD("ubyte") public bool absoluteRead(string path, out ubyte[] output) 389 { 390 void[] data; 391 bool ret = absoluteRead(path, data); 392 output = cast(ubyte[])data; 393 return ret; 394 } 395 396 public bool absoluteReadText(string path, out string output) 397 { 398 void[] data; 399 bool ret = absoluteRead(path, data); 400 if(ret) 401 output = cast(string)data; 402 return ret; 403 } 404 405 406 public bool isDir(string path){return isPathValid(path, false, false) && fs.isDir(getPath(path));} 407 public bool isFile(string path){return isPathValid(path, true, false) && fs.isFile(getPath(path));} 408 409 public string writeCache(string cacheName, void[] data) 410 { 411 import hip.util.path:joinPath; 412 string p = joinPath(initialPath, ".cache", cacheName); 413 write(p, data); 414 return p; 415 } 416 } 417 418 HipFileSystemImplementation HipFileSystem() 419 { 420 __gshared HipFileSystemImplementation fs; 421 if(!fs) 422 fs = new HipFileSystemImplementation(); 423 return fs; 424 } 425 426 export extern(C) IHipFS HipFileSystemAPI() 427 { 428 return HipFileSystem(); 429 } 430 431 alias HipFS = HipFileSystem;